在开发大型应用的时候,页面一般会划分为多个部分,但对不同的页面而言,可能有相同的部分(如头部导航,页脚)。
若每个页面都独立开发,无疑会增加开发成本,因此一般会把页面的不同部分拆分为独立的组件,然后在不同的页面共享这些组件,达到复用效果。
通常而言,一个应用会以一棵嵌套的组件树的形式来组织,如下图:

例如,你可能会有页头、侧边栏、内容这些组件,它们可能又包含了其它如导航链接、博文之类的组件。
组件基础
组件的特点
Vue 中组件的特点如下:
- 组件可以进行任意次数的复用
- 组件相当于 Vue 实例,能接收如
data、computed、watch、methods以及生命周期钩子等(除el外)属性。 组件的
data必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝,如:1
2
3
4
5data: function () {
return {
count: 0
}
}每个组件都会各自独立的维护它的
count。因为你每用一次组件,就会有一个它的新实例被创建。
组件的基本使用步骤
组件使用分为三个步骤:
- ① 创建组件构造器
- ② 注册组件
- ③ 使用组件
1 | <div id="app"> |
化繁为简——语法糖
前面编写组件的方法太繁琐,官方推荐使用语法糖来省略部分代码,当然其本质还是会调用Vue.extend:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<div id="app">
<!-- 2. 使用组件-->
<my-cpn></my-cpn>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
// 1. 注册组件同时创建组件构造器:第一个参数是注册组件的标签名,第二个参数组件构造器
Vue.component('my-cpn', {
template: `<div><h1>组件的使用</h1></div>`
});
const vue = new Vue({
el: "#app"
})
</script>
组件的类型
在模板中使用组件,其必须先注册才能被 Vue 识别 ,组件的注册类型又分为 2 种:
- 全局组件
- 局部组件
全局组件
全局注册的组件可以在其被注册之后的任何(通过new Vue)新创建的 Vue 根实例中使用,也包括其组件树中的所有子组件的模板中,通过Vue.component即可注册:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<div id="app">
<my-btn></my-btn>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
// 全局组件
Vue.component("my-btn", {
data() {
return {
count: 0,
}
},
template: `<button @click='count ++'>点击该按钮次数增加:{{count}}</button>`
});
const vue = new Vue({
el: "#app",
})
</script>
全局组件是可复用的 Vue 实例,且带有一个名字:在这个例子中是my-btn。
我们通过new Vue创建的 Vue 根实例中,可以把这个组件作为自定义元素来使用。
局部组件(常用)
局部组件使用变量赋值的方式定义,之后通过在 Vue 实例中的components属性添加它:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<div id="app">
<my-component></my-component>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
const component = {
data() {
return {
count: 0
}
},
template: '<button @click='count ++'>点击该按钮次数增加:{{count}}</button>'
};
const vue = new Vue({
el: '#app',
components: {
'my-component': component
}
})
</script>
当然,语法糖使得其可以简写如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16...
<script>
const vue = new Vue({
el: "#app",
components: {
"my-component": {
data() {
return {
count: 0,
}
},
template: "<button @click='count ++'>点击该按钮次数增加:{{count}}</button>"
};
}
})
</script>
注意哦:局部组件只能在相关 Vue 实例作用的标签中使用,且在其子组件中不可用。
例如,若你希望 ComponentA 在 ComponentB 中可用,则你需要这样写:
1 | const ComponentA = { /* ... */ } |
若通过 webpack 使用 ES 2015 模块,可以这么做:1
2
3
4
5
6
7
8import ComponentA from './ComponentA.vue'
export default {
components: {
ComponentA
},
// ...
}
注意在 ES 2015+ 中,在对象中放一个类似 ComponentA 的变量名其实是 ComponentA: ComponentA 的缩写,即这个变量名同时是:
- 用在模板中的自定义元素的名称
- 包含了这个组件选项的变量名
模版分离
由于在template模版里的代码看起来很乱,因此 Vue 提供了模版分离的 2 种方式:
- 使用
<script>标签(略) - 使用
<template>标签添加id
1 | <div id="app"> |
组件通信
由于数据间经常交互,所以不同的组件之间应该可以相互通信,组件的通信又分为:
- 父组件传递数据给子组件
- 子组件传递数据(或事件)给父组件
什么是父子组件?
组件有父子之分,子组件必须在父组件定义前定义,而父组件必须在 Vue 的实例中注册后才能使用。
Vue 实例亦叫root组件:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35<div id="app">
<!-- 4.最后使用父组件 -->
<fc></fc>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
<!-- 1.首先定义子组件 -->
const sCpn = {
template: `
<div>
<h1>我是子组件</h1>
</div>
`
};
<!-- 2.其次定义父组件并注册子组件 -->
const fCpn = {
template: `
<div>
<h1>我是父组件</h1>
<sc></sc>
</div>
`,
components: {
sc: sCpn
}
};
<!-- 3.再注册父组件 -->
const vue = new Vue({
el: "#app",
components: {
fc: fCpn
}
})
</script>
父子组件间的通信
我们知道子组件是不能直接引用父组件或 Vue 实例中的数据的,但是在开发中经常有一些数据需要从上层传递到下层。
比如在一个页面中,我们从服务器请求了大量数据。
其中一部分数据,并非由整个页面的大(父)组件来显示,而是需要让下面的小(子)组件去显示。
此时,并不会让子组件再次发送一个网络请求,而是直接让父组件将数据传递给子组件。
那么,如何进行父子组件间的通信呢?
Vue 官方对不同方向提供了不同的解决方案:
- 父传子:可通过
prop向子组件传递数据 - 子传父:可通过事件向父组件发送消息
父传子
在子组件中,可以使用prop属性来接收父组件的数据,接收后可通过在组件标签中使用v-bind属性动态地将父组件的数据注入子组件中。
官方说明:Prop 使你可以在组件上注册的一些自定义 attribute。当一个值传递给一个 prop attribute 的时候,它就变成了那个组件实例的一个属性。
Prop 传递值类型
prop传递值的类型可以为:
- 数字
- 布尔值
- 字符串
- 数组:数组中的字符串(数字、布尔值)就是传递时的名称
- 对象:对象可以设置传递时的类型,也可以设置默认值等
下面仅介绍常用的 2 种:
数组
1 | <!-- 4.使用子组件并将父组件数据注入子组件中 --> |
注意哦:这里的父组件为 Vue 实例哦,它也叫root组件呢!
经过上面的理解后,可以将代码简化:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35<!-- 4.使用子组件并将父组件数据注入子组件中 -->
<div id="app">
<cpn :products="products"></cpn>
</div>
<template id="tl">
<div>
<h1>{{products}}</h1>
<ul>
<li v-for="p in products">{{p}}</li>
</ul>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
<!-- 1.定义子组件 -->
const cpn = {
<!-- 2.引用分离的模版 -->
template: '#tl',
data() {
return {};
},
props: ['products']
};
<!-- 3.定义父组件(设置数据)并注册子组件 -->
const vm = new Vue({
el: '#app',
data: {
products: ['苹果', '三星', '华为']
},
components: {
cpn
}
})
</script>
对象
除了数组之外,亦可使用对象。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40<!-- 4.使用子组件并将父组件数据注入子组件中 -->
<div id="app">
<cpn :person="person"></cpn>
</div>
<template id="tl">
<div>
<ul>
<li>{{person.name}}</li>
<li>{{person.age}}</li>
</ul>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
<!-- 1.定义子组件 -->
const cpn = {
<!-- 2.引用分离的模版 -->
template: '#tl',
data() {
return {};
},
props: {
person: {}
}
};
<!-- 3.定义父组件(设置数据)并注册子组件 -->
const vm = new Vue({
el: '#app',
data: {
person: {
name: 'Lovike',
age: 18
}
},
components: {
cpn
}
})
</script>
类型检查
一般在需要对props进行类型等验证时,就需要该写法了,验证支持以下数据类型:
- String
- Number
- Boolean
- Array
- Object
- Date
- Function
- Symbol
1 | const cpn = { |
注意哦:子组件的props目前不支持驼峰命名,cproducts命名为cProducts可能有 Bug,官方推荐使用 kebab-case 的事件名:c-products
子传父
除了父组件向子组件传递数据外,还有一种常见的是子组件传递数据或事件到父组件中,此时需要使用自定义事件来完成。
自定义事件的流程如下:
- 在子组件中,通过
$emit()来发射事件; - 在父组件中,通过
v-on来监听子组件事件
1 | <!-- 父组件模版 --> |
组件访问
有时候我们需要直接访问组件里的一些数据,即让父组件直接访问子组件或子组件直接访问父组件。
父访子
要想让父组件访问子组件的数据,可以:
- 使用
$children - 使用
$refs(常用)
$children 方式
1 | <!-- Vue 实例模版 --> |
$refs 方式
使用该方式,需要在组件标签上添加ref属性。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28<!-- Vue 实例模版 -->
<div id="app">
<cpn></cpn>
<cpn ref="num2"></cpn>
<cpn></cpn>
<button @click="getCpnData">查看子组件数据</button>
</div>
<!-- 子组件模版 -->
<template id="cpnt">
<div>
<p>我叫{{name}},今年{{age}}岁</p>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
const vm = new Vue({
el: '#app',
methods: {
getCpnData() {
console.log(this.$refs.num2);
console.log(this.$refs.num2.name + ' ' + this.$refs.num2.age);
}
},
...
})
</script>
子访父
要想让子组件访问父组件的数据,可以使用$parent1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35<!-- Vue 实例模版 -->
<div id="app">
<p>我叫{{name}},今年{{age}}岁</p>
<cpn></cpn>
</div>
<!-- 子组件模版 -->
<template id="cpnt">
<div>
<button @click="getCpnData">查看父组件数据</button>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
name: 'lisi',
age: 18
},
components: {
cpn: {
template: '#cpnt',
methods: {
getCpnData() {
console.log(this.$parent);
console.log(this.$parent.name + ' ' + this.$parent.age);
// 访问 root 组件
console.log(this.$root);
}
},
}
}
})
</script>
注意哦:可以通过$root访问Vue实例(根)组件的数据
插槽 slot
组件的插槽能使封装的组件扩展性更强,让使用者决定组件内部的一些内容到底该显示什么。
快速入门
1 | <!-- Vue 实例模版 --> |
具名插槽
当子组件的功能复杂时,子组件的插槽可能并非时一个。
比如封装一个导航栏的子组件,可能就需要 3 个插槽,分别代表左、中、右。
那么在外面给插槽插入内容时,如何区分插入的是哪一个呢?
此时需要通过给插槽命名来解决该问题:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24<!-- Vue 实例模版 -->
<div id="app">
<cpn><span slot="left">替换左插槽</span></cpn>
</div>
<!-- 子组件模版 -->
<template id="cpnt">
<div>
<slot name="left">左</slot>
<slot name="center">中</slot>
<slot name="right">右</slot>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
const vm = new Vue({
el: '#app',
components: {
cpn: {
template: '#cpnt',
}
}
})
</script>
作用域插槽
作用域插槽可以在父组件替换插槽标签时,内容由子组件提供。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38<!-- Vue 实例模版 -->
<div id="app">
<cpn>
<template v-slot:default="fruits">
{{fruits}}
<ul>
<li v-for="f in fruits">{{f}}</li>
</ul>
</template>
</cpn>
</div>
<!-- 子组件模版 -->
<template id="cpnt">
<div>
<slot :fru="fruits">
<ul>
<li v-for="f in fruits">{{f}}</li>
</ul>
</slot>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
const vm = new Vue({
el: '#app',
components: {
cpn: {
template: '#cpnt',
data() {
return {
fruits: ['苹果', '香蕉', '橘子']
}
}
}
}
})
</script>